import katex from "../katex.mjs";

/**
 * renderA11yString returns a readable string.
 *
 * In some cases the string will have the proper semantic math
 * meaning,:
 *   renderA11yString("\\frac{1}{2}"")
 *   -> "start fraction, 1, divided by, 2, end fraction"
 *
 * However, other cases do not:
 *   renderA11yString("f(x) = x^2")
 *   -> "f, left parenthesis, x, right parenthesis, equals, x, squared"
 *
 * The commas in the string aim to increase ease of understanding
 * when read by a screenreader.
 */
var stringMap = {
  "(": "left parenthesis",
  ")": "right parenthesis",
  "[": "open bracket",
  "]": "close bracket",
  "\\{": "left brace",
  "\\}": "right brace",
  "\\lvert": "open vertical bar",
  "\\rvert": "close vertical bar",
  "|": "vertical bar",
  "\\uparrow": "up arrow",
  "\\Uparrow": "up arrow",
  "\\downarrow": "down arrow",
  "\\Downarrow": "down arrow",
  "\\updownarrow": "up down arrow",
  "\\leftarrow": "left arrow",
  "\\Leftarrow": "left arrow",
  "\\rightarrow": "right arrow",
  "\\Rightarrow": "right arrow",
  "\\langle": "open angle",
  "\\rangle": "close angle",
  "\\lfloor": "open floor",
  "\\rfloor": "close floor",
  "\\int": "integral",
  "\\intop": "integral",
  "\\lim": "limit",
  "\\ln": "natural log",
  "\\log": "log",
  "\\sin": "sine",
  "\\cos": "cosine",
  "\\tan": "tangent",
  "\\cot": "cotangent",
  "\\sum": "sum",
  "/": "slash",
  ",": "comma",
  ".": "point",
  "-": "negative",
  "+": "plus",
  "~": "tilde",
  ":": "colon",
  "?": "question mark",
  "'": "apostrophe",
  "\\%": "percent",
  " ": "space",
  "\\ ": "space",
  "\\$": "dollar sign",
  "\\angle": "angle",
  "\\degree": "degree",
  "\\circ": "circle",
  "\\vec": "vector",
  "\\triangle": "triangle",
  "\\pi": "pi",
  "\\prime": "prime",
  "\\infty": "infinity",
  "\\alpha": "alpha",
  "\\beta": "beta",
  "\\gamma": "gamma",
  "\\omega": "omega",
  "\\theta": "theta",
  "\\sigma": "sigma",
  "\\lambda": "lambda",
  "\\tau": "tau",
  "\\Delta": "delta",
  "\\delta": "delta",
  "\\mu": "mu",
  "\\rho": "rho",
  "\\nabla": "del",
  "\\ell": "ell",
  "\\ldots": "dots",
  // TODO: add entries for all accents
  "\\hat": "hat",
  "\\acute": "acute",
};
var powerMap = {
  prime: "prime",
  degree: "degrees",
  circle: "degrees",
  2: "squared",
  3: "cubed",
};
var openMap = {
  "|": "open vertical bar",
  ".": "",
};
var closeMap = {
  "|": "close vertical bar",
  ".": "",
};
var binMap = {
  "+": "plus",
  "-": "minus",
  "\\pm": "plus minus",
  "\\cdot": "dot",
  "*": "times",
  "/": "divided by",
  "\\times": "times",
  "\\div": "divided by",
  "\\circ": "circle",
  "\\bullet": "bullet",
};
var relMap = {
  "=": "equals",
  "\\approx": "approximately equals",
  "≠": "does not equal",
  "\\geq": "is greater than or equal to",
  "\\ge": "is greater than or equal to",
  "\\leq": "is less than or equal to",
  "\\le": "is less than or equal to",
  ">": "is greater than",
  "<": "is less than",
  "\\leftarrow": "left arrow",
  "\\Leftarrow": "left arrow",
  "\\rightarrow": "right arrow",
  "\\Rightarrow": "right arrow",
  ":": "colon",
};
var accentUnderMap = {
  "\\underleftarrow": "left arrow",
  "\\underrightarrow": "right arrow",
  "\\underleftrightarrow": "left-right arrow",
  "\\undergroup": "group",
  "\\underlinesegment": "line segment",
  "\\utilde": "tilde",
};

var buildString = (str, type, a11yStrings) => {
  if (!str) {
    return;
  }

  var ret;

  if (type === "open") {
    ret = str in openMap ? openMap[str] : stringMap[str] || str;
  } else if (type === "close") {
    ret = str in closeMap ? closeMap[str] : stringMap[str] || str;
  } else if (type === "bin") {
    ret = binMap[str] || str;
  } else if (type === "rel") {
    ret = relMap[str] || str;
  } else {
    ret = stringMap[str] || str;
  } // If the text to add is a number and there is already a string
  // in the list and the last string is a number then we should
  // combine them into a single number

  if (
    /^\d+$/.test(ret) &&
    a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string
    // I think we might be able to drop the nested arrays, which would make
    // this easier to type
    // $FlowFixMe
    /^\d+$/.test(a11yStrings[a11yStrings.length - 1])
  ) {
    a11yStrings[a11yStrings.length - 1] += ret;
  } else if (ret) {
    a11yStrings.push(ret);
  }
};

var buildRegion = (a11yStrings, callback) => {
  var regionStrings = [];
  a11yStrings.push(regionStrings);
  callback(regionStrings);
};

var handleObject = (tree, a11yStrings, atomType) => {
  // Everything else is assumed to be an object...
  switch (tree.type) {
    case "accent": {
      buildRegion(a11yStrings, (a11yStrings) => {
        buildA11yStrings(tree.base, a11yStrings, atomType);
        a11yStrings.push("with");
        buildString(tree.label, "normal", a11yStrings);
        a11yStrings.push("on top");
      });
      break;
    }

    case "accentUnder": {
      buildRegion(a11yStrings, (a11yStrings) => {
        buildA11yStrings(tree.base, a11yStrings, atomType);
        a11yStrings.push("with");
        buildString(accentUnderMap[tree.label], "normal", a11yStrings);
        a11yStrings.push("underneath");
      });
      break;
    }

    case "accent-token": {
      // Used internally by accent symbols.
      break;
    }

    case "atom": {
      var { text } = tree;

      switch (tree.family) {
        case "bin": {
          buildString(text, "bin", a11yStrings);
          break;
        }

        case "close": {
          buildString(text, "close", a11yStrings);
          break;
        }
        // TODO(kevinb): figure out what should be done for inner

        case "inner": {
          buildString(tree.text, "inner", a11yStrings);
          break;
        }

        case "open": {
          buildString(text, "open", a11yStrings);
          break;
        }

        case "punct": {
          buildString(text, "punct", a11yStrings);
          break;
        }

        case "rel": {
          buildString(text, "rel", a11yStrings);
          break;
        }

        default: {
          tree.family;
          throw new Error('"' + tree.family + '" is not a valid atom type');
        }
      }

      break;
    }

    case "color": {
      var color = tree.color.replace(/katex-/, "");
      buildRegion(a11yStrings, (regionStrings) => {
        regionStrings.push("start color " + color);
        buildA11yStrings(tree.body, regionStrings, atomType);
        regionStrings.push("end color " + color);
      });
      break;
    }

    case "color-token": {
      // Used by \color, \colorbox, and \fcolorbox but not directly rendered.
      // It's a leaf node and has no children so just break.
      break;
    }

    case "delimsizing": {
      if (tree.delim && tree.delim !== ".") {
        buildString(tree.delim, "normal", a11yStrings);
      }

      break;
    }

    case "genfrac": {
      buildRegion(a11yStrings, (regionStrings) => {
        // genfrac can have unbalanced delimiters
        var { leftDelim, rightDelim } = tree; // NOTE: Not sure if this is a safe assumption
        // hasBarLine true -> fraction, false -> binomial

        if (tree.hasBarLine) {
          regionStrings.push("start fraction");
          leftDelim && buildString(leftDelim, "open", regionStrings);
          buildA11yStrings(tree.numer, regionStrings, atomType);
          regionStrings.push("divided by");
          buildA11yStrings(tree.denom, regionStrings, atomType);
          rightDelim && buildString(rightDelim, "close", regionStrings);
          regionStrings.push("end fraction");
        } else {
          regionStrings.push("start binomial");
          leftDelim && buildString(leftDelim, "open", regionStrings);
          buildA11yStrings(tree.numer, regionStrings, atomType);
          regionStrings.push("over");
          buildA11yStrings(tree.denom, regionStrings, atomType);
          rightDelim && buildString(rightDelim, "close", regionStrings);
          regionStrings.push("end binomial");
        }
      });
      break;
    }

    case "hbox": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "kern": {
      // No op: we don't attempt to present kerning information
      // to the screen reader.
      break;
    }

    case "leftright": {
      buildRegion(a11yStrings, (regionStrings) => {
        buildString(tree.left, "open", regionStrings);
        buildA11yStrings(tree.body, regionStrings, atomType);
        buildString(tree.right, "close", regionStrings);
      });
      break;
    }

    case "leftright-right": {
      // TODO: double check that this is a no-op
      break;
    }

    case "lap": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "mathord": {
      buildString(tree.text, "normal", a11yStrings);
      break;
    }

    case "op": {
      var { body, name } = tree;

      if (body) {
        buildA11yStrings(body, a11yStrings, atomType);
      } else if (name) {
        buildString(name, "normal", a11yStrings);
      }

      break;
    }

    case "op-token": {
      // Used internally by operator symbols.
      buildString(tree.text, atomType, a11yStrings);
      break;
    }

    case "ordgroup": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "overline": {
      buildRegion(a11yStrings, function (a11yStrings) {
        a11yStrings.push("start overline");
        buildA11yStrings(tree.body, a11yStrings, atomType);
        a11yStrings.push("end overline");
      });
      break;
    }

    case "phantom": {
      a11yStrings.push("empty space");
      break;
    }

    case "raisebox": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "rule": {
      a11yStrings.push("rectangle");
      break;
    }

    case "sizing": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "spacing": {
      a11yStrings.push("space");
      break;
    }

    case "styling": {
      // We ignore the styling and just pass through the contents
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "sqrt": {
      buildRegion(a11yStrings, (regionStrings) => {
        var { body, index } = tree;

        if (index) {
          var indexString = flatten(buildA11yStrings(index, [], atomType)).join(
            ","
          );

          if (indexString === "3") {
            regionStrings.push("cube root of");
            buildA11yStrings(body, regionStrings, atomType);
            regionStrings.push("end cube root");
            return;
          }

          regionStrings.push("root");
          regionStrings.push("start index");
          buildA11yStrings(index, regionStrings, atomType);
          regionStrings.push("end index");
          return;
        }

        regionStrings.push("square root of");
        buildA11yStrings(body, regionStrings, atomType);
        regionStrings.push("end square root");
      });
      break;
    }

    case "supsub": {
      var { base, sub, sup } = tree;
      var isLog = false;

      if (base) {
        buildA11yStrings(base, a11yStrings, atomType);
        isLog = base.type === "op" && base.name === "\\log";
      }

      if (sub) {
        var regionName = isLog ? "base" : "subscript";
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start " + regionName);
          buildA11yStrings(sub, regionStrings, atomType);
          regionStrings.push("end " + regionName);
        });
      }

      if (sup) {
        buildRegion(a11yStrings, function (regionStrings) {
          var supString = flatten(buildA11yStrings(sup, [], atomType)).join(
            ","
          );

          if (supString in powerMap) {
            regionStrings.push(powerMap[supString]);
            return;
          }

          regionStrings.push("start superscript");
          buildA11yStrings(sup, regionStrings, atomType);
          regionStrings.push("end superscript");
        });
      }

      break;
    }

    case "text": {
      // TODO: handle other fonts
      if (tree.font === "\\textbf") {
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start bold text");
          buildA11yStrings(tree.body, regionStrings, atomType);
          regionStrings.push("end bold text");
        });
        break;
      }

      buildRegion(a11yStrings, function (regionStrings) {
        regionStrings.push("start text");
        buildA11yStrings(tree.body, regionStrings, atomType);
        regionStrings.push("end text");
      });
      break;
    }

    case "textord": {
      buildString(tree.text, atomType, a11yStrings);
      break;
    }

    case "smash": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "enclose": {
      // TODO: create a map for these.
      // TODO: differentiate between a body with a single atom, e.g.
      // "cancel a" instead of "start cancel, a, end cancel"
      if (/cancel/.test(tree.label)) {
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start cancel");
          buildA11yStrings(tree.body, regionStrings, atomType);
          regionStrings.push("end cancel");
        });
        break;
      } else if (/box/.test(tree.label)) {
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start box");
          buildA11yStrings(tree.body, regionStrings, atomType);
          regionStrings.push("end box");
        });
        break;
      } else if (/sout/.test(tree.label)) {
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start strikeout");
          buildA11yStrings(tree.body, regionStrings, atomType);
          regionStrings.push("end strikeout");
        });
        break;
      } else if (/phase/.test(tree.label)) {
        buildRegion(a11yStrings, function (regionStrings) {
          regionStrings.push("start phase angle");
          buildA11yStrings(tree.body, regionStrings, atomType);
          regionStrings.push("end phase angle");
        });
        break;
      }

      throw new Error(
        "KaTeX-a11y: enclose node with " + tree.label + " not supported yet"
      );
    }

    case "vcenter": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "vphantom": {
      throw new Error("KaTeX-a11y: vphantom not implemented yet");
    }

    case "hphantom": {
      throw new Error("KaTeX-a11y: hphantom not implemented yet");
    }

    case "operatorname": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "array": {
      throw new Error("KaTeX-a11y: array not implemented yet");
    }

    case "raw": {
      throw new Error("KaTeX-a11y: raw not implemented yet");
    }

    case "size": {
      // Although there are nodes of type "size" in the parse tree, they have
      // no semantic meaning and should be ignored.
      break;
    }

    case "url": {
      throw new Error("KaTeX-a11y: url not implemented yet");
    }

    case "tag": {
      throw new Error("KaTeX-a11y: tag not implemented yet");
    }

    case "verb": {
      buildString("start verbatim", "normal", a11yStrings);
      buildString(tree.body, "normal", a11yStrings);
      buildString("end verbatim", "normal", a11yStrings);
      break;
    }

    case "environment": {
      throw new Error("KaTeX-a11y: environment not implemented yet");
    }

    case "horizBrace": {
      buildString("start " + tree.label.slice(1), "normal", a11yStrings);
      buildA11yStrings(tree.base, a11yStrings, atomType);
      buildString("end " + tree.label.slice(1), "normal", a11yStrings);
      break;
    }

    case "infix": {
      // All infix nodes are replace with other nodes.
      break;
    }

    case "includegraphics": {
      throw new Error("KaTeX-a11y: includegraphics not implemented yet");
    }

    case "font": {
      // TODO: callout the start/end of specific fonts
      // TODO: map \BBb{N} to "the naturals" or something like that
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    case "href": {
      throw new Error("KaTeX-a11y: href not implemented yet");
    }

    case "cr": {
      // This is used by environments.
      throw new Error("KaTeX-a11y: cr not implemented yet");
    }

    case "underline": {
      buildRegion(a11yStrings, function (a11yStrings) {
        a11yStrings.push("start underline");
        buildA11yStrings(tree.body, a11yStrings, atomType);
        a11yStrings.push("end underline");
      });
      break;
    }

    case "xArrow": {
      throw new Error("KaTeX-a11y: xArrow not implemented yet");
    }

    case "cdlabel": {
      throw new Error("KaTeX-a11y: cdlabel not implemented yet");
    }

    case "cdlabelparent": {
      throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
    }

    case "mclass": {
      // \neq and \ne are macros so we let "htmlmathml" render the mathmal
      // side of things and extract the text from that.
      var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass

      buildA11yStrings(tree.body, a11yStrings, _atomType);
      break;
    }

    case "mathchoice": {
      // TODO: track which which style we're using, e.g. dispaly, text, etc.
      // default to text style if even that may not be the correct style
      buildA11yStrings(tree.text, a11yStrings, atomType);
      break;
    }

    case "htmlmathml": {
      buildA11yStrings(tree.mathml, a11yStrings, atomType);
      break;
    }

    case "middle": {
      buildString(tree.delim, atomType, a11yStrings);
      break;
    }

    case "internal": {
      // internal nodes are never included in the parse tree
      break;
    }

    case "html": {
      buildA11yStrings(tree.body, a11yStrings, atomType);
      break;
    }

    default:
      tree.type;
      throw new Error("KaTeX a11y un-recognized type: " + tree.type);
  }
};

var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
  if (a11yStrings === void 0) {
    a11yStrings = [];
  }

  if (tree instanceof Array) {
    for (var i = 0; i < tree.length; i++) {
      buildA11yStrings(tree[i], a11yStrings, atomType);
    }
  } else {
    handleObject(tree, a11yStrings, atomType);
  }

  return a11yStrings;
};

var flatten = function flatten(array) {
  var result = [];
  array.forEach(function (item) {
    if (item instanceof Array) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  });
  return result;
};

var renderA11yString = function renderA11yString(text, settings) {
  var tree = katex.__parse(text, settings);

  var a11yStrings = buildA11yStrings(tree, [], "normal");
  return flatten(a11yStrings).join(", ");
};

export default renderA11yString;
